Micron Document
🎖️GitЯра🎖️

Commit bbc85dcf76f45477480f03848c9f694969e3c8fb


Parents : 49e454a
Author : renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Signature : Signature validation error
Date : 2026-07-09T03:10:50Z
Committer : GitHub <noreply@github.com>
Date : 2026-07-09T03:10:50Z

feat(ble): update kable-core to v0.44.0 + surface missing-permission scan failures (#6165)

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: James Rich <james.a.rich@gmail.com>
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>

Changes
Diff

diff --git a/.skills/compose-ui/strings-index.txt b/.skills/compose-ui/strings-index.txt
index fdd16ebf56..341ba4288b 100644
--- a/.skills/compose-ui/strings-index.txt
+++ b/.skills/compose-ui/strings-index.txt
@@ -123,6 +123,7 @@ bluetooth_feature_config_description
bluetooth_feature_discovery
bluetooth_feature_discovery_description
bluetooth_permission
+bluetooth_scan_missing_permission
bluetooth_scan_start_failed
bold_heading
bonding_failed_permissions

diff --git a/core/ble/src/commonMain/kotlin/org/meshtastic/core/ble/BleScanStartException.kt b/core/ble/src/commonMain/kotlin/org/meshtastic/core/ble/BleScanStartException.kt
index 5c26b679b0..737b51803f 100644
--- a/core/ble/src/commonMain/kotlin/org/meshtastic/core/ble/BleScanStartException.kt
+++ b/core/ble/src/commonMain/kotlin/org/meshtastic/core/ble/BleScanStartException.kt
@@ -22,6 +22,10 @@ enum class BleScanStartFailureReason(val androidCode: String, val description: S
androidCode = "SCAN_FAILED_APPLICATION_REGISTRATION_FAILED(2)",
description = "Android could not register the app for BLE scanning",
),
+ MissingScanPermission(
+ androidCode = "MISSING_SCAN_PERMISSION",
+ description = "A runtime permission required for BLE scanning is not granted",
+ ),
}
/** A discovery scan-start failure. No advertisements can be delivered until a future scan starts successfully. */

diff --git a/core/ble/src/commonMain/kotlin/org/meshtastic/core/ble/KableBleScanner.kt b/core/ble/src/commonMain/kotlin/org/meshtastic/core/ble/KableBleScanner.kt
index c4e53bbbf5..12dfb4e70b 100644
--- a/core/ble/src/commonMain/kotlin/org/meshtastic/core/ble/KableBleScanner.kt
+++ b/core/ble/src/commonMain/kotlin/org/meshtastic/core/ble/KableBleScanner.kt
@@ -98,15 +98,20 @@ private fun Throwable.asBleScanStartExceptionOrNull(): BleScanStartException? {
var current: Throwable? = this
var depth = 0
while (current != null && depth < MAX_SCAN_START_FAILURE_CAUSE_DEPTH) {
- if (current.isApplicationRegistrationFailure()) {
- return BleScanStartException(BleScanStartFailureReason.ApplicationRegistrationFailed, this)
- }
+ val reason = current.scanStartFailureReasonOrNull()
+ if (reason != null) return BleScanStartException(reason, this)
current = current.cause
depth++
}
return null
}
+private fun Throwable.scanStartFailureReasonOrNull(): BleScanStartFailureReason? = when {
+ isApplicationRegistrationFailure() -> BleScanStartFailureReason.ApplicationRegistrationFailed
+ isMissingScanPermission() -> BleScanStartFailureReason.MissingScanPermission
+ else -> null
+}
+
// Kable exposes Android scan-start registration failure as an IllegalStateException message,
// so keep this matcher narrow and local to the scanner adapter.
private fun Throwable.isApplicationRegistrationFailure(): Boolean = this is IllegalStateException &&
@@ -114,3 +119,11 @@ private fun Throwable.isApplicationRegistrationFailure(): Boolean = this is Ille
failureMessage.contains("app cannot be registered", ignoreCase = true) ||
failureMessage.contains("SCAN_FAILED_APPLICATION_REGISTRATION_FAILED", ignoreCase = true)
} == true
+
+// Kable's scan-permission check throws a plain IllegalStateException "Missing required <permission> for scanning"
+// (ACCESS_COARSE/FINE_LOCATION on Android <12, BLUETOOTH_SCAN on 12+). Match the stable message anchors.
+private fun Throwable.isMissingScanPermission(): Boolean = this is IllegalStateException &&
+ message?.let { failureMessage ->
+ failureMessage.contains("Missing required", ignoreCase = true) &&
+ failureMessage.contains("for scanning", ignoreCase = true)
+ } == true

diff --git a/core/ble/src/commonMain/kotlin/org/meshtastic/core/ble/MeshtasticBleDevice.kt b/core/ble/src/commonMain/kotlin/org/meshtastic/core/ble/MeshtasticBleDevice.kt
index 0e2cb7140b..b4e1f03f61 100644
--- a/core/ble/src/commonMain/kotlin/org/meshtastic/core/ble/MeshtasticBleDevice.kt
+++ b/core/ble/src/commonMain/kotlin/org/meshtastic/core/ble/MeshtasticBleDevice.kt
@@ -17,7 +17,7 @@
package org.meshtastic.core.ble
import com.juul.kable.Advertisement
-import com.juul.kable.ExperimentalApi
+import com.juul.kable.ExperimentalKableApi
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.asStateFlow
@@ -54,7 +54,7 @@ class MeshtasticBleDevice(
override val rssi: Int? = advertisement?.rssi
- @OptIn(ExperimentalApi::class)
+ @OptIn(ExperimentalKableApi::class)
override suspend fun readRssi(): Int {
val active = ActiveBleConnection.active
return if (active != null && active.address == address) {

diff --git a/core/ble/src/commonTest/kotlin/org/meshtastic/core/ble/KableBleConnectionTest.kt b/core/ble/src/commonTest/kotlin/org/meshtastic/core/ble/KableBleConnectionTest.kt
index 7af428d76d..491b9d44a4 100644
--- a/core/ble/src/commonTest/kotlin/org/meshtastic/core/ble/KableBleConnectionTest.kt
+++ b/core/ble/src/commonTest/kotlin/org/meshtastic/core/ble/KableBleConnectionTest.kt
@@ -145,6 +145,16 @@ class KableBleConnectionTest {
assertEquals(BleScanStartFailureReason.ApplicationRegistrationFailed, failure.reason)
}
+ @Test
+ fun `scan wraps missing scan permission failure`() = runTest {
+ val message = "Missing required android.permission.ACCESS_COARSE_LOCATION for scanning"
+ val scanner = TestKableBleScanner(scanResults = flow { throw IllegalStateException(message) })
+
+ val failure = assertFailsWith<BleScanStartException> { scanner.scan(timeout = 1.seconds).toList() }
+
+ assertEquals(BleScanStartFailureReason.MissingScanPermission, failure.reason)
+ }
+
@Test
fun `scan preserves unrelated illegal state failure`() = runTest {
val scanner =

diff --git a/core/resources/src/commonMain/composeResources/values/strings.xml b/core/resources/src/commonMain/composeResources/values/strings.xml
index 5918d4c36b..2bfb655595 100644
--- a/core/resources/src/commonMain/composeResources/values/strings.xml
+++ b/core/resources/src/commonMain/composeResources/values/strings.xml
@@ -141,6 +141,7 @@
<string name="bluetooth_feature_discovery">Discovery</string>
<string name="bluetooth_feature_discovery_description">Find and identify Meshtastic devices near you.</string>
<string name="bluetooth_permission">Bluetooth</string>
+ <string name="bluetooth_scan_missing_permission">Bluetooth scan needs permission. Grant the Nearby devices (or Location) permission to find devices.</string>
<string name="bluetooth_scan_start_failed">Bluetooth scan couldn't start. Try again, or toggle Bluetooth if the problem continues.</string>
<string name="bold_heading">Bold Heading</string>
<string name="bonding_failed_permissions">Pairing failed. Grant nearby device permissions and try again.</string>

diff --git a/feature/connections/src/commonMain/kotlin/org/meshtastic/feature/connections/ScannerViewModel.kt b/feature/connections/src/commonMain/kotlin/org/meshtastic/feature/connections/ScannerViewModel.kt
index 80e4c38a57..24ca4fc1da 100644
--- a/feature/connections/src/commonMain/kotlin/org/meshtastic/feature/connections/ScannerViewModel.kt
+++ b/feature/connections/src/commonMain/kotlin/org/meshtastic/feature/connections/ScannerViewModel.kt
@@ -40,6 +40,7 @@ import kotlinx.coroutines.flow.update
import kotlinx.coroutines.launch
import org.meshtastic.core.ble.BleDevice
import org.meshtastic.core.ble.BleScanStartException
+import org.meshtastic.core.ble.BleScanStartFailureReason
import org.meshtastic.core.ble.BleScanner
import org.meshtastic.core.ble.MeshtasticBleConstants
import org.meshtastic.core.common.util.safeCatchingAll
@@ -58,6 +59,7 @@ import org.meshtastic.core.repository.RadioPrefs
import org.meshtastic.core.repository.ServiceRepository
import org.meshtastic.core.repository.UiPrefs
import org.meshtastic.core.resources.Res
+import org.meshtastic.core.resources.bluetooth_scan_missing_permission
import org.meshtastic.core.resources.bluetooth_scan_start_failed
import org.meshtastic.core.resources.getStringSuspend
import org.meshtastic.core.ui.viewmodel.safeLaunch
@@ -396,9 +398,13 @@ open class ScannerViewModel(
Logger.w(exception) {
"BLE scan could not start: ${exception.reason.androidCode} (${exception.reason.description})"
}
+ val messageRes =
+ when (exception.reason) {
+ BleScanStartFailureReason.MissingScanPermission -> Res.string.bluetooth_scan_missing_permission
+ BleScanStartFailureReason.ApplicationRegistrationFailed -> Res.string.bluetooth_scan_start_failed
+ }
val errorMessage =
- safeCatchingAll { getStringSuspend(Res.string.bluetooth_scan_start_failed) }
- .getOrDefault(BLE_SCAN_START_FAILURE_MESSAGE_FALLBACK)
+ safeCatchingAll { getStringSuspend(messageRes) }.getOrDefault(BLE_SCAN_START_FAILURE_MESSAGE_FALLBACK)
serviceRepository.setErrorMessage(text = errorMessage, severity = Severity.Warn)
}

diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml
index 681ebea472..fbd06ae67e 100644
--- a/gradle/libs.versions.toml
+++ b/gradle/libs.versions.toml
@@ -91,7 +91,7 @@ uri-kmp = "0.0.21"
osmdroid-android = "6.1.20"
spotless = "8.8.0"
vico = "3.3.0-next.1"
-kable = "0.43.1"
+kable = "0.44.0"
mqttastic = "0.4.0"
jmdns = "3.6.3"
qrcode-kotlin = "4.5.0"

Served by rngit 1.5.2 - Generated in 0.16s